package com.blustreaksw.cyclecoach.test;

import java.util.ArrayList;

import com.blustreaksw.cyclecoach.R;
import com.blustreaksw.cyclecoach.btle.BtleDevice;
import com.blustreaksw.cyclecoach.btle.BtleDeviceStatus;
import com.blustreaksw.cyclecoach.btle.BtleDeviceType;
import com.blustreaksw.cyclecoach.btle.BtleDevice.BtleDeviceCallback;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

public class BtleDeviceActivityDemo extends Activity {
	
	private static final String TAG = "BtleDeviceActivityDemo";
	
	Button mHrmButton;
	Button mCscButton;
	
	TextView mHrmName;
	TextView mHrmBPM;
	TextView mCscName;
	TextView mCscSpeed;
	TextView mCscCadence;
	
	private int mLastWheelRevs = -1;
	private int mLastCrankRevs = -1;
	private long mLastCscTime = -1;
	private int mLastWheelTime = -1;
	private int mLastCrankTime = -1;
	
	private ArrayList<Integer> mWheelRevs;
	private ArrayList<Integer> mWheelTime;
	private ArrayList<Integer> mCrankRevs;
	private ArrayList<Integer> mCrankTime;
	
	private final int ROLLING_AVG_COUNT = 6;
	private final int ROLLING_AVG_COUNT_WHEEL = 4;
	
	
	private BtleDevice mHrmBtleDevice;
	private BtleDevice mCscBtleDevice;
	
	private final String HRM_ADDRESS = "F5:3A:70:C6:CB:17";
	private final String CSC_ADDRESS = "DC:6B:67:04:21:93";
	private final double WHEEL_CIRCUMFERENCE = 2.096;
	private final int UNSIGNED_INT16_MAX_VALUE = 65536;
	
	Activity mParent;
	
	@Override
	public void onCreate(Bundle savedInstanceState){
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_btledevicedemo);
		mParent = this;
		
		mHrmName = (TextView)findViewById(R.id.hrmName);
		mHrmBPM = (TextView)findViewById(R.id.hrmBPM);
		mHrmButton = (Button)findViewById(R.id.hrmButton);
		mHrmButton.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				if(mHrmButton.getText().equals("Connect")) {
					mHrmButton.setEnabled(false);
					mHrmBtleDevice = new BtleDevice(
							HRM_ADDRESS, BtleDeviceType.HEART_RATE_MONITOR,
							mDeviceCallback, getApplicationContext());
					if(mHrmBtleDevice != null) {
						mHrmBtleDevice.initiateDeviceConnection();
					}
				}
				else {
					mHrmBtleDevice.disableSensorDataUpload();
					
					final BtleDevice hrmDevice = mHrmBtleDevice;
					mHrmBtleDevice = null;
					mHrmButton.setText("Connect");
					mHrmButton.setEnabled(false);
					
					mParent.runOnUiThread(new Runnable() {

						@Override
						public void run() {
							hrmDevice.closeDeviceConnection();
							mHrmButton.setEnabled(true);
						}
						
					});
					
					mHrmName.setText("N/A");
					mHrmBPM.setText("N/A");
				}
			}
			
		});
		
		mCscName = (TextView)findViewById(R.id.cscName);
		mCscSpeed = (TextView)findViewById(R.id.cscSpeed);
		mCscCadence = (TextView)findViewById(R.id.cscCadence);
		mCscButton = (Button)findViewById(R.id.cscButton);
		mCscButton.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				if(mCscButton.getText().equals("Connect")) {
					mCscButton.setEnabled(false);
					mCscBtleDevice = new BtleDevice(
							CSC_ADDRESS, BtleDeviceType.CYCLING_SPEED_CADENCE_SENSOR,
							mDeviceCallback, getApplicationContext());
					if(mCscBtleDevice != null) {
						mCscBtleDevice.initiateDeviceConnection();
					}
				} else {
					mCscBtleDevice.disableSensorDataUpload();
					
					final BtleDevice cscDevice = mCscBtleDevice;
					mCscBtleDevice = null;
					mCscButton.setText("Connect");
					mCscButton.setEnabled(false);
					
					mParent.runOnUiThread(new Runnable() {

						@Override
						public void run() {
							cscDevice.closeDeviceConnection();
							mCscButton.setEnabled(true);
						}
					});
					
					mCscName.setText("N/A");
					mCscSpeed.setText("N/A");
					mCscCadence.setText("N/A");
				}
					
			}
		});
	}
	
	private void reconnectHrmDevice() {
		if(mHrmButton.getText().equals("Disconnect")) {
			mHrmButton.setEnabled(false);
			mHrmBtleDevice = new BtleDevice(
					HRM_ADDRESS, BtleDeviceType.HEART_RATE_MONITOR,
					mDeviceCallback, getApplicationContext());
			if(mHrmBtleDevice != null) {
				mHrmBtleDevice.initiateDeviceConnection();
			}
		}
	}
	
	private void reconnectCscDevice() {
		if(mCscButton.getText().equals("Disconnect")) {
			mCscButton.setEnabled(false);
			mCscBtleDevice = new BtleDevice(
					HRM_ADDRESS, BtleDeviceType.CYCLING_SPEED_CADENCE_SENSOR,
					mDeviceCallback, getApplicationContext());
			if(mCscBtleDevice != null) {
				mCscBtleDevice.initiateDeviceConnection();
			}
		}
	}
	
	@Override
	public void onPause() {
		super.onPause();
		if(mHrmButton.getText().equals("Disconnect")) {
			mHrmButton.setEnabled(false);
			mHrmButton.setText("Connect");
			
			if(mHrmBtleDevice != null) {
				mHrmBtleDevice.disableSensorDataUpload();
				mHrmBtleDevice.closeDeviceConnection();
				mHrmBtleDevice = null;
			}
		}
		
		if(mCscButton.getText().equals("Disconnect")) {
			mCscButton.setEnabled(false);
			mCscButton.setText("Connect");
			
			if(mCscBtleDevice != null) {
				mCscBtleDevice.disableSensorDataUpload();
				mCscBtleDevice.closeDeviceConnection();
				mCscBtleDevice = null;
			}
		}
	}
	
	@Override
	public void onResume() {
		super.onResume();
		initializeRollingAvgLists();
		reconnectHrmDevice();
		reconnectCscDevice();
		
	}
	
	public void updateHrmTextView(final long timestamp, final int bpm) {
		mParent.runOnUiThread(new Runnable() {

			@Override
			public void run() {
				mHrmBPM.setText(bpm + " BPM");
			}
		});
		
	}
	
	public void updateCscTextView(
			long timestamp, final int cumWheelRevs, final int cumCrankRevs,
				final int wheelTime, final int crankTime) {
		
		double wheelRollingAvg = updateWheelRollingAvg(cumWheelRevs, wheelTime);
		double crankRollingAvg = updateCrankRollingAvg(cumCrankRevs, crankTime);
		
		if(mLastWheelRevs < 0) {
			mLastWheelRevs = cumWheelRevs;
			mLastCrankRevs = cumCrankRevs;
			mLastWheelTime = wheelTime;
			mLastCrankTime = crankTime;
			mLastCscTime = timestamp;
			return;
		}
		
		double crankTimeElapsed;
		double wheelTimeElapsed;
		double speedInMetersPerSec;
		double cranksPerSec;
		
		
		if(wheelTime == mLastWheelTime) {
			// no data received
			wheelTimeElapsed = 0;
			speedInMetersPerSec = 0;
			
		} else {
			if(wheelTime > mLastWheelTime) {
				wheelTimeElapsed = ((double)(wheelTime - mLastWheelTime)) / 1024.00;
			} else {
				wheelTimeElapsed = ((double)(wheelTime + 65536 - mLastWheelTime)) / 1024.00;
			}

			
			speedInMetersPerSec = 
					(((double)(cumWheelRevs - mLastWheelRevs)) * WHEEL_CIRCUMFERENCE) / wheelTimeElapsed;
			//Log.d(TAG, String.format("speed meters/sec, instant: %1.4f, rollingAvg: %1.4f", speedInMetersPerSec, wheelRollingAvg));
		}
		
		if(crankTime == mLastCrankTime) {
			// no data received
			crankTimeElapsed = 0;
			cranksPerSec = 0;
		}
		else { 
			if(crankTime > mLastCrankTime) {
				crankTimeElapsed = ((double)(crankTime - mLastCrankTime)) / 1024.00;
			} else {
				crankTimeElapsed = ((double)(crankTime + 65536 - mLastCrankTime)) / 1024.00;
			}
			
			cranksPerSec = ((double)(cumCrankRevs - mLastCrankRevs)) / crankTimeElapsed;			
			
			//Log.d(TAG, String.format("cranks per sec, instant: %1.4f, rollingAvg: %1.4f", cranksPerSec, crankRollingAvg));
		}
		
		//Log.d(TAG, String.format("time elapsed, crank: %1.4f, wheel: %1.4f", crankTimeElapsed, wheelTimeElapsed));
		
		final int crankRPM = ((int) Math.ceil(60.0 * cranksPerSec));
		final double speedKPH = (speedInMetersPerSec * 3.600);
		
		final int crankRPMrollingAvg = ((int) Math.ceil(60.0 * crankRollingAvg));
		final double speedKPHrollingAvg = (wheelRollingAvg * 3.600);
		
		//Log.d(TAG, String.format("speed KPH, instant: %1.4f, rollingAvg: %1.4f", speedKPH, speedKPHrollingAvg));
		//Log.d(TAG, String.format("cranks RPM, instant: %d, rollingAvg: %d", crankRPM, crankRPMrollingAvg));
		
		mParent.runOnUiThread(new Runnable() {

			@Override
			public void run() {
				if(speedKPH == 0.0) {
					mCscSpeed.setText(String.format("%1.2f KPH", speedKPHrollingAvg));
				} else {
					mCscSpeed.setText(String.format("%1.2f KPH", speedKPH));
				}
				
				if(crankRPM == 0) {
					mCscCadence.setText(crankRPMrollingAvg + " RPM");
				} else {
					mCscCadence.setText(crankRPM + " RPM");
				}
			}
		});
		
		mLastWheelRevs = cumWheelRevs;
		mLastCrankRevs = cumCrankRevs;
		mLastWheelTime = wheelTime;
		mLastCrankTime = crankTime;
		mLastCscTime = timestamp;
	}
	
	BtleDevice.BtleDeviceCallback mDeviceCallback = new BtleDevice.BtleDeviceCallback() {

		@Override
		public void transferCharacteristicData_heartRate(
				BtleDevice device, long timestamp, int sessionId, int bpm) {
			
			updateHrmTextView(timestamp, bpm);
		}

		@Override
		public void transferCharacteristicData_cyclePower(BtleDevice device,
				long timestamp,
				int sessionId, int instantPower, double pedalPowerBalance,
				double accumulatedTorque, int cumWheelRevs, int cumCrankRevs,
				int lastWheelTime, int lastCrankTime,
				int maxForce, int minForce, double maxTorque, double minTorque,
				int maxAngle, int minAngle, int topDeadSpotAngle,
				int bottomDeadSpotAngle, int accumulatedEnergy) {
			
			
			
		}

		@Override
		public void transferCharacteristicData_cycleSpeedCadence(BtleDevice device, long timestamp,
				int sessionId, int cumWheelRevs, int cumCrankRevs, int lastWheelTime, int lastCrankTime) {

			updateCscTextView(timestamp, cumWheelRevs, cumCrankRevs, lastWheelTime, lastCrankTime);
		}

		@Override
		public void onSuccessfulDeviceConnection(BtleDevice btleDevice) {
			Log.d(TAG, "onSuccessfulDeviceConnection " + btleDevice.getBtleDeviceAddress());
			btleDevice.enableSensorDataUpload(1);
			final String devName = btleDevice.getDeviceName();
			if(btleDevice.getBtleDeviceAddress().equals(HRM_ADDRESS)) {
				mParent.runOnUiThread(new Runnable() {

					@Override
					public void run() {
						mHrmButton.setText("Disconnect");
						mHrmButton.setEnabled(true);
						mHrmName.setText(devName);
					}
				});
			} else if(btleDevice.getBtleDeviceAddress().equals(CSC_ADDRESS)) {
				mParent.runOnUiThread(new Runnable() {

					@Override
					public void run() {
						mCscButton.setText("Disconnect");
						mCscButton.setEnabled(true);
						mCscName.setText(devName);
					}
				});	
			}
		}

		@Override
		public void onFailedDeviceConnection(BtleDevice btleDevice, final BtleDeviceStatus error) {
			
			final String address = btleDevice.getBtleDeviceAddress();
			if(btleDevice.getBtleDeviceAddress().equals(HRM_ADDRESS)) {
				mParent.runOnUiThread(new Runnable() {

					@Override
					public void run() {
						mHrmButton.setText("Connect");
						mHrmButton.setEnabled(true);
						mHrmName.setText("N/A");
						mHrmBPM.setText("N/A");
					}
				});
			} else if(btleDevice.getBtleDeviceAddress().equals(CSC_ADDRESS)) {
				mParent.runOnUiThread(new Runnable() {

					@Override
					public void run() {
						mCscButton.setText("Connect");
						mCscButton.setEnabled(true);
						mCscName.setText("N/A");
						mCscSpeed.setText("N/A");
						mCscCadence.setText("N/A");
					}
				});	
			}
			
			mParent.runOnUiThread(new Runnable() {

				@Override
				public void run() {
					Toast.makeText(mParent.getBaseContext(), address + "\n" + error, Toast.LENGTH_LONG).show();
				}
				
			});
			
		}

		@Override
		public void onDeviceConnectionDropped(final BtleDevice btleDevice, final BtleDeviceStatus error) {
			
			final String address = btleDevice.getBtleDeviceAddress();		
			
			if(address.equals(HRM_ADDRESS)) {
				mParent.runOnUiThread(new Runnable() {

					@Override
					public void run() {
						mHrmButton.setText("Connect");
						mHrmButton.setEnabled(true);
						mHrmName.setText("N/A");
						mHrmBPM.setText("N/A");
					}
				});
			} else if(address.equals(CSC_ADDRESS)) {
				mParent.runOnUiThread(new Runnable() {

					@Override
					public void run() {
						mCscButton.setText("Connect");
						mCscButton.setEnabled(true);
						mCscName.setText("N/A");
						mCscSpeed.setText("N/A");
						mCscCadence.setText("N/A");
					}
				});	
			}
			
			mParent.runOnUiThread(new Runnable() {

				@Override
				public void run() {
					Toast.makeText(mParent.getBaseContext(), address + "\n" + error, Toast.LENGTH_LONG).show();
				}
				
			});
		}

	};
	
	private void initializeRollingAvgLists() {
		mWheelRevs = new ArrayList<Integer>();
		mWheelTime = new ArrayList<Integer>();
		mCrankRevs = new ArrayList<Integer>();
		mCrankTime = new ArrayList<Integer>();
	}
	
	private double updateWheelRollingAvg(int rev, int time) {
		double sumRev = 0;
		double sumTime = 0;
		
		if(mWheelRevs.size() < ROLLING_AVG_COUNT_WHEEL) {
			mWheelRevs.add(rev);
			mWheelTime.add(time);
		} else {
			mWheelRevs.remove(0);
			mWheelTime.remove(0);
			mWheelRevs.add(rev);
			mWheelTime.add(time);
		}
		
		StringBuilder wheelRevInfo = new StringBuilder();
		StringBuilder wheelTimeInfo = new StringBuilder();
		
		
		if(mWheelRevs.size() <= 1) {
			return 0.0;
		} else {
			// Calculate a sum of the revolution/time differences in the list
			for(int i = 1; i < mWheelRevs.size(); i++) {
				if(mWheelRevs.get(i) >= mWheelRevs.get(i - 1)) {
					sumRev += (mWheelRevs.get(i) - mWheelRevs.get(i - 1));
					wheelRevInfo.append(String.format("%d ", mWheelRevs.get(i) - mWheelRevs.get(i - 1)));
				} else {
					// Wheel revs is stored as unsigned 32-bit integer on device,
					// so this indicates the value has rolled over.
					// TODO need to fix this in the future to handle values over 2^31
					
				}
				
				if(mWheelTime.get(i) >= mWheelTime.get(i - 1)) {
					sumTime += (mWheelTime.get(i) - mWheelTime.get(i - 1));
					wheelTimeInfo.append(String.format("%d ", mWheelTime.get(i) - mWheelTime.get(i - 1)));
				} else {
					// Time is stored as an unsigned 16-bit short on device,
					// so this indicates the value has rolled over.
					sumTime += (mWheelTime.get(i) + UNSIGNED_INT16_MAX_VALUE - mWheelTime.get(i - 1));
					wheelTimeInfo.append(String.format("%d ", mWheelTime.get(i) + UNSIGNED_INT16_MAX_VALUE - mWheelTime.get(i - 1)));
				}
			}
		}
		
		//Log.d("updateWheelRollingAvg", "sumRev: " + sumRev + " " + wheelRevInfo.toString());		
		//Log.d("updateWheelRollingAvg", "sumTime: " + sumTime + " " + wheelTimeInfo.toString());
		//Log.d("updateWheelRollingAvg", "rollingAvgSpeed: " + String.format("%1.2f", (sumRev * 1024 / sumTime * WHEEL_CIRCUMFERENCE)));
		
		if(sumTime > 0) {
			return (sumRev * 1024 / sumTime * WHEEL_CIRCUMFERENCE);
		} else {
			return 0;
		}
	}
	
	private double updateCrankRollingAvg(int rev, int time) {
		double sumRev = 0;
		double sumTime = 0;
		
		if(mCrankRevs.size() < ROLLING_AVG_COUNT) {
			mCrankRevs.add(rev);
			mCrankTime.add(time);
		} else {
			mCrankRevs.remove(0);
			mCrankTime.remove(0);
			mCrankRevs.add(rev);
			mCrankTime.add(time);
		}
		
		
		if(mCrankRevs.size() <= 1) {
			return 0.0;
		} else {
			// Calculate a sum of the revolution/time differences in the list
			for(int i = 1; i < mCrankRevs.size(); i++) {
				if(mCrankRevs.get(i) >= mCrankRevs.get(i-1)) {
					sumRev += (mCrankRevs.get(i) - mCrankRevs.get(i - 1));
				} else {
					// Crank revs is an unsigned 16-bit short on device, so
					// this indicates the value has rolled over.
					sumTime += (mCrankRevs.get(i) + UNSIGNED_INT16_MAX_VALUE - mCrankRevs.get(i - 1));
				}
				
				if(mCrankTime.get(i) >= mCrankTime.get(i - 1)) {
					sumTime += (mCrankTime.get(i) - mCrankTime.get(i - 1));
				} else {
					// Time is stored as an unsigned 16-bit short on device,
					// so this indicates the value has rolled over.
					sumTime += (mCrankTime.get(i) + UNSIGNED_INT16_MAX_VALUE - mCrankTime.get(i - 1));
				}
				
			}
		}
		
		if(sumTime > 0) {
			return sumRev * 1024 / sumTime;
		} else {
			return 0;
		}
	}

}
